/*
 * CCVisu is a tool for visual graph clustering
 * and general force-directed graph layout.
 * This file is part of CCVisu.
 *
 * Copyright (C) 2005-2012  Dirk Beyer
 *
 * CCVisu is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * CCVisu is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with CCVisu; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please find the GNU Lesser General Public License in file
 * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt
 *
 * Dirk Beyer    (firstname.lastname@uni-passau.de)
 * University of Passau, Bavaria, Germany
 */
package org.sosy_lab.ccvisu.ui.controlpanel;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkElementIndex;
import static com.google.common.base.Preconditions.checkNotNull;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
import java.util.EventObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.AbstractCellEditor;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.RowFilter;
import javax.swing.ScrollPaneConstants;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableRowSorter;

import org.sosy_lab.ccvisu.graph.GraphData;
import org.sosy_lab.ccvisu.graph.GraphVertex;
import org.sosy_lab.ccvisu.graph.GraphVertex.Shape;
import org.sosy_lab.ccvisu.graph.Group;
import org.sosy_lab.ccvisu.graph.NameVisibility.Priority;
import org.sosy_lab.ccvisu.graph.interfaces.GraphEventListener;
import org.sosy_lab.ccvisu.ui.helper.ColorComboBox;
import org.sosy_lab.ccvisu.ui.helper.ShapeComboBox;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutDISP;
import org.sosy_lab.util.Colors;

import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;

@SuppressWarnings("serial")
public class ToolPanelNodeManager extends ControlPanel implements GraphEventListener {

  /** The column headings. */
  private static String[]                       COLUMN_HEADINGS = { "", "Group", "Label", "InDegree", "OutDegree" };

  /** The column which contains the vertex' label. */
  private final static int                     COLUMN_LABEL    = 2;

  /** The default width of the columns. */
  private static Integer[]                      COLUMNS_WIDTH   = { 1, 50, 300, 25, 25 };

  /** The table itself. */
  private final JTable                          table           = new JTable();

  /** The model of the table. */
  private final GraphVertexTableModel           tableModel      = new GraphVertexTableModel();

  /** The text field for the filter. */
  private final JTextField                      filterRegex     = new JTextField();

  /** The row sorter of the table.*/
  private TableRowSorter<GraphVertexTableModel> sorter;

  /** Handle to the writer */
  private final WriterDataLayoutDISP            writer;

  /** The graph */
  private final GraphData                       graph;

  /**
   * Creates a new control panel containing the node manager.
   *
   * @param ccvisuOptions the global options
   */
  public ToolPanelNodeManager(WriterDataLayoutDISP writer, GraphData graph) {
    Preconditions.checkNotNull(graph);

    this.graph = graph;
    this.writer = writer;

    // listen to graph and group changes (to update the table)
    graph.addOnGraphChangeListener(this);
    graph.addOnGroupChangeListener(this);

    // set layout and add table and filter
    setLayout(new BorderLayout(5, 5));

    final JComponent tablePane = createTableComponent();
    add(tablePane, BorderLayout.CENTER);

    final JPanel filterPanel = new JPanel();
    filterPanel.setLayout(new BorderLayout());

    final JComponent filterArea = createFilterComponent();
    final JComponent applyToFilteredComponent = createApplyToNodesComponent();
    filterPanel.add(filterArea, BorderLayout.NORTH);
    filterPanel.add(applyToFilteredComponent, BorderLayout.CENTER);

    add(filterPanel, BorderLayout.SOUTH);
  }

  /**
   * Informs the table about changes of the graph and groups.
   */
  @Override
  public void onGraphChangedEvent(EventObject event) {
    if (graph == null || graph.getVertices() == null) {
      return;
    }

    List<GraphVertex> vertices = graph.getVertices();

    synchronized (vertices) {
      // update table model
      if (tableModel != null) {
        tableModel.setData(vertices);
      }
    }
  }

  /**
   * Creates a new component containing the table.
   * @return the component
   */
  private final JComponent createTableComponent() {
    // set the tables properties
    table.setDefaultRenderer(Color.class, new ColorTableCellRenderer());
    table.setDefaultEditor(Color.class, new ColorEditor());

    // fill the table with the graph's vertices
    List<GraphVertex> vertices = graph.getVertices();
    synchronized (vertices) {
      tableModel.setData(vertices);
    }
    table.setModel(tableModel);

    // set the columns default width
    for (int i = 0; i < COLUMN_HEADINGS.length; i++) {
      TableColumn column = table.getColumnModel().getColumn(i);
      column.setPreferredWidth(COLUMNS_WIDTH[i]);
    }

    // create table pane
    JScrollPane tablePane = new JScrollPane(table);
    int asNeeded = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED;
    tablePane.setHorizontalScrollBarPolicy(asNeeded);
    asNeeded = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED;
    tablePane.setVerticalScrollBarPolicy(asNeeded);

    return tablePane;
  }

  /**
   * Creates a new component containing the filter field.
   * @return the component
   */
  private final JComponent createFilterComponent() {
    JPanel form = new JPanel();
    form.setLayout(new GridBagLayout());

    // add the filter regex field and a corresponding label
    addOptionControls(form, "Filter Text: ", filterRegex);

    // create a new filter every time the content of the regex field changes
    filterRegex.getDocument().addDocumentListener(new DocumentListener() {
      @Override
      public void changedUpdate(DocumentEvent event) {
        updateFilter();
      }

      @Override
      public void insertUpdate(DocumentEvent event) {
        updateFilter();
      }

      @Override
      public void removeUpdate(DocumentEvent event) {
        updateFilter();
      }

      private void updateFilter() {
        if (sorter == null) {
          return;
        }

        if (tableModel.getRowCount() < 1) {
          sorter.setRowFilter(null);
          return;
        }

        RowFilter<GraphVertexTableModel, Object> rowFilter = null;
        try {
          rowFilter = RowFilter.regexFilter(filterRegex.getText(), COLUMN_LABEL);

        } catch (java.util.regex.PatternSyntaxException e) {
          return;
        }
        sorter.setRowFilter(rowFilter);
      }
    });

    return form;
  }

  private final JComponent createApplyToNodesComponent() {
    JPanel form = new JPanel();
    form.setLayout(new GridBagLayout());

    final ShapeComboBox shapeComboBox = new ShapeComboBox(Shape.DISC);
    final ColorComboBox colorComboBox = new ColorComboBox(Colors.GREEN.get());

    shapeComboBox.setColor(colorComboBox.getSelectedColor());
    colorComboBox.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        shapeComboBox.setColor(colorComboBox.getSelectedColor());
        shapeComboBox.repaint();
      }
    });

    final JCheckBox showLabelCheckBox = new JCheckBox("Yes");
    final JCheckBox nodeVisibleCheckBox = new JCheckBox("Yes");

    // defaults
    showLabelCheckBox.setSelected(false);
    nodeVisibleCheckBox.setSelected(true);

    JButton applyToNodesButton = new JButton("Apply to filtered nodes!");

    addOptionControls(form, "Node visible?", nodeVisibleCheckBox);
    addOptionControls(form, "Show labels?", showLabelCheckBox);

    addOptionControls(form, "Color: ", colorComboBox);
    colorComboBox.setToolTipText("The color of the filtered nodes.");

    addOptionControls(form, "Shape: ", shapeComboBox);
    shapeComboBox.setToolTipText("The shape of the filtered nodes.");

    addOptionControls(form, "", applyToNodesButton);
    applyToNodesButton.setToolTipText("Apply the selected attributes to all filtered nodes.");

    applyToNodesButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {

        if (sorter == null) {
          return;
        }

        for (int rowIndex = 0; rowIndex < sorter.getViewRowCount(); rowIndex++) {
          int modelRow = sorter.convertRowIndexToModel(rowIndex);
          int vertexId = tableModel.getVertexId(modelRow);
          GraphVertex vertex = graph.getVertexById(vertexId);

          vertex.setColor(colorComboBox.getSelectedColor());
          vertex.setShowVertex(nodeVisibleCheckBox.isSelected());
          vertex.setShape(shapeComboBox.getSelectedShape());
          vertex.setShowName(Priority.FIND, showLabelCheckBox.isSelected());
        }

        graph.notifyAboutLayoutChange(new EventObject(this));
        writer.getDisplay().getCanvas().updateAndPaint();
      }
    });

    TitledBorder border = BorderFactory.createTitledBorder("Set attributes of filtered nodes.");
    form.setBorder(new CompoundBorder(new EmptyBorder(10, 10, 10, 10), border));

    return form;
  }

  /**
   * The table model for vertices.
   */
  private class GraphVertexTableModel extends AbstractTableModel {

    private Object[][] data;

    /** Maps the index of a vertex in the object table to the id of the vertex. */
    private Map<Integer, Integer> indexToIdMap;

    /** The column that contains the color. */
    private static final int COLUMN_COLOR = 0;

    public GraphVertexTableModel() {
      data = new Object[0][0];
      indexToIdMap = Maps.newHashMap();
    }

    @Override
    public int getRowCount() {
      return data.length;
    }

    @Override
    public int getColumnCount() {
      return COLUMN_HEADINGS.length;
    }

    @Override
    public String getColumnName(int column) {
      checkElementIndex(column, getColumnCount());

      return COLUMN_HEADINGS[column];
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
      checkElementIndex(rowIndex, getRowCount());
      checkElementIndex(columnIndex, getColumnCount());

      return data[rowIndex][columnIndex];
    }

    public int getVertexId(int row) {
      checkArgument(row >= 0 || row < data.length, "Illegal parameter range.");

      return indexToIdMap.get(row);
    }

    public void setData(List<GraphVertex> vertices) {
      setData(formatTableData(vertices));
    }

    private void setData(Object[][] data) {
      checkNotNull(data);

      for (Object[] row : data) {
        checkNotNull(row, "parameter may not contain null rows.");

        for (Object cell : row) {
          checkNotNull(cell, "parameter may not contain null columns.");
        }
      }

      // test if update is necessary
      if (Arrays.deepEquals(this.data, data)) {
        // the model will not be changed. The effect is that marked vertices
        // are still marked when only the layout, not the graph has changed.
        return;
      }

      // prepare update
      int oldRowCount = this.data.length;
      int newRowCount = data.length;

      // update
      this.data = data;

      // inform about update
      if (newRowCount > oldRowCount) {
        // there are new rows
        fireTableRowsInserted(oldRowCount, newRowCount - 1);
        // the old ones may have been updated, too
        fireTableRowsUpdated(0, Math.max(0, oldRowCount - 1));

      } else if (newRowCount < oldRowCount && newRowCount > 0) {
        // some rows were deleted
        fireTableRowsDeleted(newRowCount, oldRowCount);
        // the remaining may have been updated, too
        fireTableRowsUpdated(0, Math.max(0, newRowCount - 1));

      } else if (newRowCount > 0) {
        // there are rows, and parts/all of them were updated
        fireTableRowsUpdated(0, Math.max(0, newRowCount - 1));

      } else {
        // there were or there are no rows
      }

      if (newRowCount > 0) {
        if (sorter == null) {
          sorter = new TableRowSorter<GraphVertexTableModel>(tableModel);
          sorter.setSortsOnUpdates(true);
        }

        filterRegex.setEditable(true);
        filterRegex.setFocusable(true);

      } else {
        sorter = null;
        filterRegex.setEditable(false);
        filterRegex.setFocusable(false);
      }
      table.setRowSorter(sorter);
    }

    @Override
    public Class<?> getColumnClass(int column) {
      return getValueAt(0, column).getClass();
    }

    @Override
    public boolean isCellEditable(int row, int col) {
      if (col == COLUMN_COLOR && row >= 0) {
        return true;
      }

      return false;
    }

    private Object[][] formatTableData(List<GraphVertex> vertices) {

      checkNotNull(vertices);

      Object[][] tableData = new Object[0][0];
      indexToIdMap = new HashMap<Integer, Integer>(vertices.size());

      synchronized (vertices) {

        int i = 0;
        tableData = new Object[vertices.size()][COLUMN_HEADINGS.length];

        for (GraphVertex vertex : vertices) {
          if (!vertex.isAuxiliary()) {
            tableData[i][COLUMN_COLOR] = vertex.getColor();

            Group group = graph.findGroupOfVertex(vertex);
            if (group != null) {
              tableData[i][1] = group.getName();
            } else {
              tableData[i][1] = "unknown";
            }

            tableData[i][2] = vertex.getLabel();
            tableData[i][3] = vertex.getDegree();
            tableData[i][4] = vertex.getDegreeOut();

            indexToIdMap.put(i, vertex.getId());

            ++i;
          }
        }
      }

      return tableData;
    }
  }

  /**
   * A cell renderer for cells containing an object of type color.
   */
  private class ColorTableCellRenderer extends JLabel implements TableCellRenderer {

    public ColorTableCellRenderer() {
      setOpaque(true);
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
        boolean isSelected, boolean hasFocus, int row, int column) {

      Color color = (Color) value;

      setBackground(color);
      setText("");
      setToolTipText(Colors.toString(color));

      return this;
    }
  }

  /**
   * ColorEditor allows to change the color of a vertex in the Node Manager.
   */
  private class ColorEditor extends AbstractCellEditor
      implements TableCellEditor, ActionListener {

    private Color currentColor;
    private int currentRow;
    private JButton button;
    private JColorChooser colorChooser;
    private JDialog dialog;
    private static final String EDIT = "edit";

    public ColorEditor() {
      button = new JButton();
      button.setActionCommand(EDIT);
      button.addActionListener(this);
      button.setBorderPainted(false);

      //Set up the dialog that the button brings up.
      colorChooser = new JColorChooser();
      dialog = JColorChooser.createDialog(button, "Select Color", true,
          colorChooser, this, null);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
      if (EDIT.equals(e.getActionCommand())) {
        // The user has clicked the cell, so bring up the dialog.
        button.setBackground(currentColor);
        colorChooser.setColor(currentColor);
        dialog.setVisible(true);

        // Make the renderer reappear.
        fireEditingStopped();

      } else {
        // User pressed dialog's "OK" button.
        int row = sorter.convertRowIndexToModel(currentRow);
        int currentId = tableModel.getVertexId(row);
        GraphVertex vertex = graph.getVertexById(currentId);
        vertex.setColor(colorChooser.getColor());

        graph.notifyAboutLayoutChange(new EventObject(this));
        currentColor = colorChooser.getColor();
      }
    }

    @Override
    public Object getCellEditorValue() {
      return currentColor;
    }

    @Override
    public Component getTableCellEditorComponent(JTable table, Object value,
        boolean isSelected, int row, int column) {

      currentRow = row;
      currentColor = (Color)value;
      return button;
    }
  }
}
